//*********************************************************************
//		Copyright (c) 2003 - 2008 Microsoft Corporation.
//      This code is provided under Microsoft Public License, which can be found at
//      http://www.microsoft.com/resources/sharedsource/licensingbasics/publiclicense.mspx 
//
// @File: PerfProvider.cs
//
// Purpose: Imports perf data
//
//
//
// Notes: 
//
//
//	
// History: 
//     @Version: V2.1
//
//     07/17/08 PGV 2.1.001.0 Starting V2.1 (based on 2.0.030.0 code)
//
//
// @EndHeader@
//********************************************************************* 

using System;
using System.IO;
using System.Diagnostics;
using Microsoft.Win32;
using System.Data;
using System.Data.SqlClient;

namespace H2V2
{
	public struct FileMarks 
	{
		public int computerFirstLine;
		public int computerLastLine;
		public int numberOfComputers;
		public int counterFirstLine;
		public int counterLastLine;
		public int numberOfCounters;
		public int dataFirstLine;

		public void Reset ()
		{
			computerFirstLine=0;
			computerLastLine=0;
			numberOfComputers=0;
			counterFirstLine=0;
			counterLastLine=0;
			numberOfCounters=0;
			dataFirstLine=0;
		}
	}

	public class ProcessErrorException : Exception 
	{
        public CollectorError error;

        public ProcessErrorException (CollectorError a_err, string a_msg)
            : base (a_msg)
		{
			error = a_err;
		}
	}

    public class ProviderErrorException : Exception
    {
        public CollectorError error;

        public ProviderErrorException (CollectorError a_err, string a_msg)
            : base (a_msg)
        {
            error = a_err;
        }
    }

    public class PerfProvider : StatusObject
	{
		string m_machine; 
		Guid m_signature;
		int m_host_id;
		string m_path;
		bool IsCleanable=true;
		Guid LockerGUID=Guid.NewGuid ();
		int m_cur_fid;
		string CurrentFile,
			CurFileName,
			OutputDir;

		public PerfProvider (string a_machine)
		{
			m_machine = a_machine;

			RegistryKey rk = RegistryKey.OpenRemoteBaseKey (RegistryHive.LocalMachine, m_machine);

            rk = rk.OpenSubKey("SOFTWARE");
            rk = rk.OpenSubKey("Microsoft");
            rk = rk.OpenSubKey("SQLH2PerfCollector");

            if (rk == null)
            {   // There is no subkey
                throw new ProviderErrorException ( CollectorError.CannotFindRegistryKey, "Couldn't find required registry key on Perf Provider " + m_machine);  
            }

			string s = (string)rk.GetValue ("iGUID");
			m_signature = new Guid (s);

			CurrentFile = (string)rk.GetValue ("CurrentFile");
			OutputDir = (string)rk.GetValue ("OutputDir");

			CurFileName = Path.GetFileName (CurrentFile);

			if (0 == String.Compare (a_machine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
			{
				m_path = OutputDir;
			}
			else 
			{
				m_path = "\\\\"+m_machine+"\\SQLH2PerfCollectorData$\\";
			}

		}

		public void Init ()
		{
			RegisterHost ();
		}

		void RegisterHost ()
		{
			SqlCommand rcmd;
			SqlParameter p_host_id;
			SqlParameter retval;
			int retcode;


			rcmd = new SqlCommand ("dbo.spr_phost", H2Manager.Repository.SQLConnection);
			rcmd.CommandType = CommandType.StoredProcedure;

			retval = new SqlParameter ("retvalue", SqlDbType.Int);
			retval.Direction = ParameterDirection.ReturnValue;
			rcmd.Parameters.Add (retval);
            rcmd.Parameters.AddWithValue ("@name", Environment.MachineName);
            rcmd.Parameters.AddWithValue ("@signature", m_signature);
			p_host_id = new SqlParameter ("@host_id", SqlDbType.Int);
			p_host_id.Direction = ParameterDirection.Output;
			rcmd.Parameters.Add (p_host_id);

			rcmd.ExecuteNonQuery ();
			try 
			{
				retcode = (int)retval.Value;
				m_host_id = (int)p_host_id.Value;
				ChangeStatus ("Perf host_id = "+m_host_id, StatusEventType.INFO);
			}
			catch (SqlException sqlex)
			{
				H2Manager.Log.SQLException (sqlex);
				ChangeStatus (sqlex.StackTrace, StatusEventType.STACK_TRACE);
				throw (new StorageErrorException (CollectorError.StorSyntaxErrorReaderSP,sqlex.Number,"dbo.spr_phost"));
			}
		}

		public void Load ()
		{
			int r,
				q_errors = 0;
			FileMarks stfm = new FileMarks ();

			DirectoryInfo di = new DirectoryInfo (m_path);

			int pid = getLock ();
			if (pid == 0) 
			{
				return;
			}

			CleanTempTables (pid); // in case something left after the last loading

			foreach (FileInfo fi in di.GetFiles ("pf*.txt"))
			{
				if (fi.Name == CurFileName) continue;
				try
				{
					if (CheckFile (fi)) 
					{
						if (fi.Length == 0)
						{
							UpdateFileState (m_cur_fid, 101);
							Clean (fi);
							continue;
						}
						UpdateLock (pid, m_cur_fid);
						stfm.Reset ();
						if (PrepareBcp (fi, ref stfm))
						{
							int iretry = 0;
							do
							{
								if (iretry > 0) ChangeStatus ("Retrying to load the file", StatusEventType.INFO);
								try
								{
									r = RunBcp (fi, stfm, pid);
								}
								catch (ProcessErrorException perr)
								{
									if (perr.error == CollectorError.PerfProviderBcpFailure)
									{
										CleanTempTables (pid);
										++iretry;
										UpdateFileRetry (m_cur_fid);
									}
									else
									{
										throw perr;
									}
								}
							} while (iretry > 0 && iretry < 2); // 1 retry

							UpdateFileState (m_cur_fid, 100);
							Clean (fi);
						}
						else
						{
							++q_errors;
							UpdateFileState (m_cur_fid, -1); // incomplete/corrupted file
						}
					}
				}
				catch (ProcessErrorException perr)
				{
                    if (perr.error == CollectorError.PerfProviderCannotClean || perr.error == CollectorError.ExternalCannotFindBcp || perr.error == CollectorError.PerfProviderBcpCopyInFailure)
					{	// Unable to clean temp tables for 1005, 1006
                        if (perr.error == CollectorError.PerfProviderBcpCopyInFailure)
						{
							++q_errors;
							CleanTempTables (pid);
						}
						releaseLock (pid);
						throw new ProviderErrorException (CollectorError.PerfProviderUnableToContinue, "PerfProvider is unable to continue");
					}
					else 
					{	// Timeout perr.error = 1003, 1004
						++q_errors;
						CleanTempTables (pid);
						UpdateFileRetry (m_cur_fid);	// Controlled exception during processing
						//  mark file for retry
					}
				}
				catch (Exception ex)
				{
					ChangeStatus (ex.Message, StatusEventType.ERROR);
					++q_errors;
					CleanTempTables (pid);
					UpdateFileState (m_cur_fid, -3); // exception during processing
				}
			}
			releaseLock (pid);

            if (q_errors > 0)
            {
                throw new ProviderErrorException (CollectorError.PerfProviderErrorsEncountered, "Errors were encountered loading Perf data from " + m_machine);
            }
		}

		bool CheckFile (FileInfo a_fi)
		{ // was it already loaded?
			SqlCommand rcmd;
			SqlParameter param1,
				param2,
				param3,
				param4,
				param5;
			SqlParameter retval;
			int state = 0,
				retry = 0;

			rcmd = new SqlCommand ("dbo.spp_regfile", H2Manager.Repository.SQLConnection);
			rcmd.CommandType = CommandType.StoredProcedure;

			retval = new SqlParameter ("retvalue", SqlDbType.Int);
			retval.Direction = ParameterDirection.ReturnValue;
			rcmd.Parameters.Add (retval);
			param1 = new SqlParameter ("@host_id", SqlDbType.Int);
			param1.Value = m_host_id; 
			rcmd.Parameters.Add (param1);
			param2 = new SqlParameter ("@fname", SqlDbType.NVarChar);
			param2.Value = a_fi.FullName;
			rcmd.Parameters.Add (param2);
			param3 = new SqlParameter ("@fid", SqlDbType.Int);
			param3.Direction = ParameterDirection.Output;
			rcmd.Parameters.Add (param3);
			param4 = new SqlParameter ("@state", SqlDbType.Int);
			param4.Direction = ParameterDirection.Output;
			rcmd.Parameters.Add (param4);
			param5 = new SqlParameter ("@retry", SqlDbType.Int);
			param5.Direction = ParameterDirection.Output;
			rcmd.Parameters.Add (param5);

			rcmd.ExecuteNonQuery ();
			try 
			{
				m_cur_fid = (int)param3.Value;
				state = (int)param4.Value;
				retry = (int)param5.Value;
				ChangeStatus (a_fi.FullName+": fid = "+m_cur_fid+", state = "+state+", retry = "+retry,
					StatusEventType.INFO);
			}
			catch (SqlException sqlex)
			{
				H2Manager.Log.SQLException (sqlex);
				ChangeStatus (sqlex.StackTrace, StatusEventType.STACK_TRACE);
				throw (new StorageErrorException (CollectorError.StorSyntaxErrorReaderSP,sqlex.Number,"dbo.spp_regfile"));
			}

			return ((state == 0) && (retry < 2));
		}

		bool PrepareBcp (FileInfo a_fi, ref FileMarks a_fm)
		{
			//This function scans file to find 'file marks' -
			// start and end of 'Computers' and 'Counters' sections
			// so we know what lines to load to what tables

			int i=0;
			string line;

			FileStream f = new FileStream (a_fi.FullName, FileMode.Open, FileAccess.Read);
			StreamReader s = new StreamReader (f);

			try
			{
				do
				{
					line = s.ReadLine ();
					++i;
				} while (!line.StartsWith("<COMPUTERS>"));

				a_fm.computerFirstLine = i+1;
				do
				{
					line = s.ReadLine ();
					++i;
				} while (!line.StartsWith("</COMPUTERS>"));
				a_fm.computerLastLine = i-1;
				a_fm.numberOfComputers = a_fm.computerLastLine-a_fm.computerFirstLine+1;

				do
				{
					line = s.ReadLine ();
					++i;
				} while (!line.StartsWith("<COUNTERS>"));

				a_fm.counterFirstLine = i+1;
				do
				{
					line = s.ReadLine ();
					++i;
				} while (!line.StartsWith("</COUNTERS>"));
				a_fm.counterLastLine = i-1;
				a_fm.numberOfCounters = a_fm.counterLastLine-a_fm.counterFirstLine+1;

				a_fm.dataFirstLine = i+1;
			}
			catch (EndOfStreamException)
			{ // if we're here file has no value to us 
				ChangeStatus (a_fi.FullName+" - file corrupted or incomplete", StatusEventType.ERROR);
				H2Manager.LogMgrError (0, 0, RunErrorClass.PerfProvider, 2, a_fi.FullName, 0, 0, "file corrupted or incomplete");
				return false;
			}
			finally
			{
				s.Close ();
			}
			return true;
		}

		int RunBcp (FileInfo a_fi, FileMarks a_stfm, int pid)
		{
			int r = 0;

			Process p=new Process ();
			p.StartInfo.FileName = "bcp.exe";
			p.StartInfo.CreateNoWindow = true;
			p.StartInfo.UseShellExecute = false;
			p.StartInfo.RedirectStandardOutput = true;

			try
			{
				RunBcpCmd (p, "pt_guid"+pid, a_fi.FullName, 0, 1);
				RunBcpCmd (p, "pt_computer"+pid, a_fi.FullName, a_stfm.computerFirstLine, a_stfm.computerLastLine);
				RunBcpCmd (p, "pt_counter"+pid, a_fi.FullName, a_stfm.counterFirstLine, a_stfm.counterLastLine);
				RunBcpCmd (p, "pt_perf"+pid, a_fi.FullName, a_stfm.dataFirstLine, 0);
			}
			catch (System.ComponentModel.Win32Exception wex)
			{
				string msg = wex.Message;
				if (wex.NativeErrorCode == 2)
				{
					msg = "Could not find bcp.exe. (You can install BCP by installing SQL Server Client Tools on the system.)";
				}
				ChangeStatus (msg, StatusEventType.ERROR);
                H2Manager.LogMgrError (0, 0, RunErrorClass.PerfProvider, 1, "Perf Provider", CollectorError.ExternalCannotFindBcp, wex.NativeErrorCode, msg);
                throw new ProcessErrorException (CollectorError.ExternalCannotFindBcp, msg);
			}
			catch (ProcessErrorException)
			{
				throw;
			}
			catch (Exception ex)
			{
				ChangeStatus (ex.Message, StatusEventType.ERROR);
				ChangeStatus (ex.StackTrace, StatusEventType.STACK_TRACE);
                H2Manager.LogMgrError (0, 0, RunErrorClass.PerfProvider, 1, "Perf Provider", CollectorError.PerfProviderBcpFailure, 0, ex.Message);
                throw new ProcessErrorException (CollectorError.PerfProviderBcpFailure, "bcp failed");
			}
			finally
			{
				p.Close ();
			}

			r = KickServerProc (pid);

			return r;
		}

		void RunBcpCmd (Process a_p, string a_table, string a_fname, int a_firstLine, int a_lastLine)
		{
			string args,
				output;

			args = Constants.QuotedRepositoryDB+".dbo."+a_table+" in "+a_fname
				+" -S"+Constants.RepositoryServer+" -T -c -t, -CRAW";
			if (a_firstLine > 0)
			{
				args += " -F"+a_firstLine;

			}
			if (a_lastLine > 0)
			{
				args += " -L"+a_lastLine;
			}
			if (CmdLineArg.bBcpRSwitch)
			{
				args += " -R ";
			}

			a_p.StartInfo.Arguments = args;
			ChangeStatus (a_p.StartInfo.Arguments, StatusEventType.DEBUG);

			output = RunProcess (a_p);
			if (output.IndexOf ("BCP copy in failed") > 0)
			{
				ChangeStatus (output, StatusEventType.INFO);
				ChangeStatus ("BCP copy in failed", StatusEventType.ERROR);
                throw new ProcessErrorException (CollectorError.PerfProviderBcpCopyInFailure, "BCP copy in failed");
			}
		}

		string RunProcess (Process a_p)
		{
			a_p.Start ();
			a_p.WaitForExit();
			string output = a_p.StandardOutput.ReadToEnd();
			ChangeStatus (output, StatusEventType.DEBUG);
			return output;
		}

		#region Locks and State
		int getLock ()
		{
			SqlCommand rcmd;
			SqlParameter param;
			SqlParameter retval;
			int pid = 0, 
				r = 0;


			rcmd = new SqlCommand ("dbo.spp_getlock", H2Manager.Repository.SQLConnection);
			rcmd.CommandType = CommandType.StoredProcedure;

			retval = new SqlParameter ("retvalue", SqlDbType.Int);
			retval.Direction = ParameterDirection.ReturnValue;
			rcmd.Parameters.Add (retval);
            rcmd.Parameters.AddWithValue ("@Locker", LockerGUID);
			param = new SqlParameter ("@pid", SqlDbType.Int);
			param.Direction = ParameterDirection.Output;
			param.Value = 0;
			rcmd.Parameters.Add (param);

			r = rcmd.ExecuteNonQuery();
			try 
			{
				if (param.Value == System.DBNull.Value)
				{
                    H2Manager.LogMgrError (0, 0, RunErrorClass.PerfProvider, 1, "Perf Provider", CollectorError.PerfProviderLocked, 0, "Unable to obtain Lock to load perf data");
                    throw new ProcessErrorException (CollectorError.PerfProviderLocked, "Unable to obtain Lock to load perf data");
				}

				pid = (int)param.Value;
				ChangeStatus ("Lock aquired: pid = "+pid, StatusEventType.INFO);
			}
			catch (SqlException sqlex)
			{
				H2Manager.Log.SQLException (sqlex);
				ChangeStatus (sqlex.StackTrace, StatusEventType.STACK_TRACE);
				throw (new StorageErrorException (CollectorError.StorSyntaxErrorReaderSP,sqlex.Number,"dbo.spp_getlock"));
			}

			return pid;
		}

		bool releaseLock (int a_pid)
		{
			SqlCommand rcmd;
			SqlParameter retval;
			int ret = 0,
				r = 0;

			rcmd = new SqlCommand ("dbo.spp_releaselock", H2Manager.Repository.SQLConnection);
			rcmd.CommandType = CommandType.StoredProcedure;

			retval = new SqlParameter ("retvalue", SqlDbType.Int);
			retval.Direction = ParameterDirection.ReturnValue;
			rcmd.Parameters.Add (retval);
            rcmd.Parameters.AddWithValue ("@Locker", LockerGUID);
            rcmd.Parameters.AddWithValue ("@pid", a_pid);

			r = rcmd.ExecuteNonQuery ();
			try 
			{
				ret = (int)retval.Value;
				if (ret == 1) ChangeStatus ("Lock released: pid = "+a_pid, StatusEventType.INFO);

			}
			catch (SqlException sqlex)
			{
				H2Manager.Log.SQLException (sqlex);
				ChangeStatus (sqlex.StackTrace, StatusEventType.STACK_TRACE);
				throw (new StorageErrorException (CollectorError.StorSyntaxErrorReaderSP,sqlex.Number,"dbo.spp_releaselock"));
			}

			return (ret == 1);
		}

		void UpdateLock (int pid, int fid)
		{
			string cmd = "Update dbo.ps_dispatcher Set fStamp=GetDate(), fid="+fid
				+" Where pid="+pid+" and Locker='"+LockerGUID+"'"; 

			H2Manager.Repository.StoreNonQuery (cmd);
		}

		void UpdateFileState (int fid, int state)
		{
			string cmd = "Update dbo.p_files Set state="+state
				+" Where fid="+fid;

			H2Manager.Repository.StoreNonQuery (cmd);
		}

		void UpdateFileRetry (int fid)
		{
			string cmd = "Update dbo.p_files Set retry=retry+1 Where fid="+fid;

			H2Manager.Repository.StoreNonQuery (cmd);
		}

		#endregion

		#region Server Procs
		int KickServerProc (int a_pid)
		{ 
			int retryPrep = 0,
				retryIns = 0,
				qInserted = 0,
				r = 0;

			do 
			{
				if (retryPrep > 0)
				{
					ChangeStatus ("Retrying Perf data prepare");
				}
				try
				{
					ProcPrep (a_pid);
				}
				catch (SqlException sqlex)
				{
					if (sqlex.Message.StartsWith("Timeout expired"))
					{
						ChangeStatus ("Timeout expired preparing Perf data", StatusEventType.ERROR);
						++retryPrep;
					}
					else
					{
						throw sqlex;
					}
				}
			} while (retryPrep > 0 && retryPrep < 2);
			if (retryPrep == 2)
			{
                H2Manager.LogMgrError (0, 0, RunErrorClass.PerfProvider, 1, "Perf Provider", CollectorError.PerfProviderPrepareTimeout, 0, "Subsequent timeouts received preparing Perf data");
                throw new ProcessErrorException (CollectorError.PerfProviderPrepareTimeout, "Subsequent timeouts received preparing Perf data");
			}

			do 
			{
				if (retryIns > 0)
				{
					ChangeStatus ("Retrying Perf data prepare");
				}
				try
				{
					qInserted = ProcInsertPerf (a_pid);
				}
				catch (SqlException sqlex)
				{
					if (sqlex.Message.StartsWith("Timeout expired"))
					{
						ChangeStatus ("Timeout expired while preparing Perf data", StatusEventType.ERROR);
						++retryIns;
					}
					else
					{
						throw sqlex;
					}
				}
			} while (retryIns > 0 && retryIns < 2);
			if (retryPrep == 2)
			{
                H2Manager.LogMgrError (0, 0, RunErrorClass.PerfProvider, 1, "Perf Provider", CollectorError.PerfProviderInsertTimeout, 0, "Subsequent timeouts received inserting Perf data");
                throw new ProcessErrorException (CollectorError.PerfProviderInsertTimeout, "Subsequent timeouts received inserting Perf data");
			}

			r = ProcUpdateStats (qInserted);
			CleanTempTables (a_pid);

			return qInserted;
		}

		void ProcPrep (int a_pid)
		{
			H2Manager.Repository.StoreNonQuery ("Exec spp_prep"+a_pid+" @fid="+m_cur_fid);
		}

		int ProcInsertPerf (int a_pid)
		{
			SqlCommand rcmd;
			SqlParameter retval,
				fid;
			int r=0,
				ret=0;

			rcmd = new SqlCommand ("dbo.spp_insert_perf"+a_pid, H2Manager.Repository.SQLConnection);
			rcmd.CommandType = CommandType.StoredProcedure;

			retval = new SqlParameter ("retvalue", SqlDbType.Int);
			retval.Direction = ParameterDirection.ReturnValue;
			rcmd.Parameters.Add (retval);
			fid = new SqlParameter ("@fid", SqlDbType.Int);
			fid.Value = m_cur_fid;
			rcmd.Parameters.Add (fid);
			rcmd.CommandTimeout = 60;

			r = rcmd.ExecuteNonQuery ();
			try 
			{
				ret = (int)retval.Value;
				ChangeStatus ("   Perf records loaded: "+@ret);
			}
			catch (SqlException sqlex)
			{
				H2Manager.Log.SQLException (sqlex);
				ChangeStatus (sqlex.StackTrace, StatusEventType.STACK_TRACE);
				throw (new StorageErrorException (CollectorError.StorSyntaxErrorReaderSP,sqlex.Number,"dbo.spp_insert_perf"+a_pid));
			}

			return ret;
		}

		int ProcUpdateStats (int a_q)
		{
			return H2Manager.Repository.StoreNonQuery ("Exec spp_update_stats @fid="+m_cur_fid+", @q="+a_q);
		}

		#endregion

		#region Clean
		void Clean (FileInfo a_fi)
		{
			if (!Constants.DeleteFiles) return;

			if (IsCleanable) 
			{
				try
				{
					a_fi.Delete ();
				}
				catch (Exception ex)
				{
					ChangeStatus (ex.Message, StatusEventType.ERROR);
					ChangeStatus ("Can't delete "+a_fi.FullName+Environment.NewLine
						+"H2 will not try to delete any other files in the directory", StatusEventType.WARNING);
					IsCleanable = false;
				}
			}
		}

		void CleanTempTables (int a_pid)
		{
			try
			{
				H2Manager.Repository.StoreNonQuery ("Exec spp_clean"+a_pid);
			}
			catch (Exception ex)
			{
				ChangeStatus (ex.Message, StatusEventType.ERROR);
				ChangeStatus (ex.StackTrace, StatusEventType.STACK_TRACE);
				H2Manager.LogMgrError (0, 0, RunErrorClass.PerfProvider, 1, "Perf Provider", CollectorError.PerfProviderCannotClean, 0, "Unable to clean temp tables");
                throw new ProcessErrorException (CollectorError.PerfProviderCannotClean, "Unable to clean temp tables");
			}
		}

		#endregion
	}
}
